﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RayTracer.Structures;
using Processing.Structures;
using System.Xml;

namespace RayTracer.Environment
{
    /// <summary>
    /// Modelliert die Kamera eines Viewports.
    /// </summary>
    [Serializable]
    public class Camera
    {
        #region Typen
        /// <summary>
        /// Hält die Metriken für eine spezifische Kameraeinstellung.
        /// </summary>
        public struct CameraMetrics
        {
            #region Attribute
            /// <summary>
            /// Gibt und setzt den Fokusfaktor
            /// </summary>
            public float Focus;

            /// <summary>
            /// Gibt und setzt die Blickwinkel X-Achse.
            /// </summary>
            public Vector XAxis;

            /// <summary>
            /// Gibt und setzt die Blickwinkel Y-Achse.
            /// </summary>
            public Vector YAxis;

            /// <summary>
            /// Gibt und setzt die Blickwinkel Z-Achse.
            /// </summary>
            public Vector ZAxis;
            #endregion Attribute

            #region Methoden
            /// <summary>
            /// Initialisiert die Struktur und berechnet die Metriken.
            /// </summary>
            /// <param name="fovAngle">Der Blickfeldwinkel.</param>
            /// <param name="direction">Der Richtungswinkel.</param>
            /// <param name="topDirection">Der Obenrichtungswinkel</param>
            public CameraMetrics(float fovAngle, Vector direction, Vector topDirection)
            {
                Focus = 1.0f / (float)Math.Tan((fovAngle * Math.PI / 180.0f) / 2.0f);
                ZAxis = direction.Clone();
                XAxis = (ZAxis / topDirection).Normalized;
                YAxis = (XAxis / ZAxis).Normalized;
            }
            #endregion Methoden
        }
        #endregion Typen

        #region Attribute
        private Point position;
        private Vector direction;
        private Vector topDirection;
        private float fovAngle;
        private Point lookAtPosition;

        private CameraMetrics metrics;
        private bool metricsUpToDate = false;
        #endregion Attribute

        #region Eigenschaften
        /// <summary>
        /// Gibt die Position.
        /// </summary>
        public Point Position { get { return position; } }

        /// <summary>
        /// Gibt die Blickrichtung.
        /// </summary>
        public Vector Direction { get { return direction; } }

        /// <summary>
        /// Gibt die Richtung die Oben darstellt.
        /// </summary>
        public Vector TopDirection { get { return topDirection; } }

        /// <summary>
        /// Gibt und setzt den Blickwinkel. (1-180)
        /// </summary>
        public float FocusOfView
        {
            get { return fovAngle; }
            set
            {
                if (value < 1 && value > 180) throw new RayTracerException("Der Blickfeldwinkel muss einschließlich zwischen 1° und 180° liegen.");
                fovAngle = value;
                metricsUpToDate = false;
            }
        }

        /// <summary>
        /// Gibt den Zielpunkt an.
        /// </summary>
        public Point LookAtPosition { get { return lookAtPosition; } }

        /// <summary>
        /// Gibt die Kamerametriken an.
        /// </summary>
        public CameraMetrics Metrics { get { if (!metricsUpToDate) metrics = new CameraMetrics(fovAngle, direction, topDirection); metricsUpToDate = true; return metrics; } }
        #endregion Eigenschaften

        #region Methoden
        /// <summary>
        /// Instanziiert und initialisiert die Kamera.
        /// </summary>
        public Camera()
        {
            position = new Point(0, 0, 0);
            direction = new Vector(0, 0, -1.0f);
            topDirection = new Vector(0, 1.0f, 0);
            fovAngle = 50;
            lookAtPosition = position + direction;
        }

        /// <summary>
        /// Richtet die Kamera in eine spezifische Richtung.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="direction">Die Richtung der Kamera.</param>
        /// <param name="topDirection">Die Richtung nach Oben der Kamera.</param>
        /// <param name="fovAngle">Der Blickfeldwinkel.</param>
        public void DirectTo(Point position, Vector direction, Vector topDirection, float fovAngle)
        {
            if (direction.Length == 0) throw new RayTracerException("Die Länge der Richtung darf nicht 0 sein.");
            if (topDirection.Length == 0) throw new RayTracerException("Die Länge der Richtung nach Oben darf nicht 0 sein.");
            if (direction.Normalized == topDirection.Normalized) throw new RayTracerException("Die Blickrichtung und die Richtung nach Oben dürfen nicht gleich sein.");
            if (fovAngle < 1 && fovAngle > 180) throw new RayTracerException("Der Blickfeldwinkel muss einschließlich zwischen 1° und 180° liegen.");

            this.position = position;
            this.direction = direction.Normalized;
            this.topDirection = topDirection.Normalized;
            this.lookAtPosition = position + direction;
            this.fovAngle = fovAngle;

            metricsUpToDate = false;
        }

        /// <summary>
        /// Richtet die Kamera in eine spezifische Richtung.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="direction">Die Richtung der Kamera.</param>
        /// <param name="topPosition">Gibt eine Position über der Kamera an aus welcher die Richtung oben ermittelt wird.</param>
        /// <param name="fovAngle">Der Blickfeldwinkel.</param>
        public void DirectTo(Point position, Vector direction, Point topPosition, float fovAngle)
        {
            if (position == topPosition) throw new RayTracerException("Die Position überhalb der Kamera darf nicht gleich der Position der Kamera sein.");
            DirectTo(position, direction, topPosition - position, fovAngle);
        }


        /// <summary>
        /// Richtet die Kamera in eine spezifische Richtung.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="direction">Die Richtung der Kamera.</param>
        /// <param name="topPosition">Gibt eine Position über der Kamera an aus welcher die Richtung oben ermittelt wird.</param>
        public void DirectTo(Point position, Vector direction, Point topPosition) { DirectTo(position, direction, topPosition, fovAngle); }

        /// <summary>
        /// Richtet die Kamera in eine spezifische Richtung.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="direction">Die Richtung der Kamera.</param>
        /// <param name="topDirection">Die Richtung nach Oben der Kamera.</param>
        public void DirectTo(Point position, Vector direction, Vector topDirection) { DirectTo(position, direction, topDirection, fovAngle); }

        /// <summary>
        /// Ändert die Position der Kamera und richtet die Kamera in die bestehende Richtung.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        public void DirectTo(Point position) { DirectTo(position, direction, topDirection); }

        /// <summary>
        /// Richtet die Kamera auf einen spezifischen Punkt.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="lookAtPosition">Der Zielpunkt der Kamera.</param>
        /// <param name="topDirection">Die Richtung nach Oben der Kamera.</param>
        /// <param name="fovAngle">Der Blickfeldwinkel.</param>
        public void LookAt(Point position, Point lookAtPosition, Vector topDirection, float fovAngle)
        {
            if (position == lookAtPosition) throw new RayTracerException("Die Kameraposition darf nicht der LookAt-Position entsprechen!");
            if (topDirection.Length == 0) throw new RayTracerException("Die Länge der Richtung nach Oben darf nicht 0 sein.");
            if (fovAngle < 1 && fovAngle > 180) throw new RayTracerException("Der Blickfeldwinkel muss einschließlich zwischen 1° und 180° liegen.");

            this.position = position;
            this.topDirection = topDirection.Normalized;
            this.fovAngle = fovAngle;
            this.lookAtPosition = lookAtPosition;
            this.direction = (lookAtPosition - position).Normalized;

            metricsUpToDate = false;
        }

        /// <summary>
        /// Richtet die Kamera auf einen spezifischen Punkt.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="lookAtPosition">Der Zielpunkt der Kamera.</param>
        /// <param name="topPosition">Gibt eine Position über der Kamera an aus welcher die Richtung oben ermittelt wird.</param>
        /// <param name="fovAngle">Der Blickfeldwinkel.</param>
        public void LookAt(Point position, Point lookAtPosition, Point topPosition, float fovAngle)
        {
            if (position == topPosition || lookAtPosition == topPosition) throw new RayTracerException("Die Position überhalb der Kamera darf nicht gleich dem Zielpunkt oder der Position der Kamera sein.");
            LookAt(position, lookAtPosition, topPosition - position, fovAngle);
        }

        /// <summary>
        /// Richtet die Kamera auf einen spezifischen Punkt.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="lookAtPosition">Der Zielpunkt der Kamera.</param>
        /// <param name="topPosition">Gibt eine Position über der Kamera an aus welcher die Richtung oben ermittelt wird.</param>
        public void LookAt(Point position, Point lookAtPosition, Point topPosition) { LookAt(position, lookAtPosition, topPosition, fovAngle); }

        /// <summary>
        /// Richtet die Kamera auf einen spezifischen Punkt.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="lookAtPosition">Der Zielpunkt der Kamera.</param>
        /// <param name="topDirection">Die Richtung nach Oben der Kamera.</param>
        public void LookAt(Point position, Point lookAtPosition, Vector topDirection) { LookAt(position, lookAtPosition, topDirection, fovAngle); }

        /// <summary>
        /// Ändert die Position der Kamera und richtet die Kamera auf den bestehenden spezifischen Punkt.
        /// </summary>
        /// <param name="position">Die Position der Kamera.</param>
        /// <param name="topDirection">Die Richtung nach Oben der Kamera.</param>                
        public void LookAt(Point position, Vector topDirection) { LookAt(position, lookAtPosition, topDirection); }

        #region Typenumwandlungen
        /// <summary>
        /// Gibt eine Zeichenkettenrepräsentation der Klasse zurück.
        /// </summary>
        /// <returns>Die Zeichenkette.</returns>
        public override string ToString()
        {
            return "Position: " + Position.ToString() +
                " LookAtPosition: " + LookAtPosition.ToString() +
                " TopDirection: " + TopDirection.ToString() +
                " FocusOfView: " + FocusOfView.ToString() + "°";
        }

        /// <summary>
        /// Erstellt eine Kamera aus einem XML-Knoten.
        /// </summary>
        /// <param name="xmlNode">Der XML-Knoten.</param>
        /// <returns>Die Kamera.</returns>
        internal static Camera FromXml(XmlNode xmlNode)
        {
           Camera cam = new Camera();

           XmlNode nodePosition = xmlNode.SelectSingleNode("Position");
           XmlNode nodeLookAtPosition = xmlNode.SelectSingleNode("LookAtPosition");
           XmlNode nodeTopDirection = xmlNode.SelectSingleNode("TopDirection");
           XmlNode nodeFocusOfView = xmlNode.SelectSingleNode("FocusOfView");

           Point position = cam.Position;
           Point lookAtPosition = cam.LookAtPosition;
           Vector topDirection = cam.TopDirection;
           float fovAngle = cam.FocusOfView;

           if (nodePosition != null) position = Point.FromXml(nodePosition);
           if (nodeLookAtPosition != null) lookAtPosition = Point.FromXml(nodeLookAtPosition);
           if (nodeTopDirection != null) topDirection = Vector.FromXml(nodeTopDirection);
           if (nodeFocusOfView != null) float.TryParse(nodeFocusOfView.InnerText, out fovAngle);

           cam.LookAt(position, lookAtPosition, topDirection, fovAngle);

           return cam;
        }

        /// <summary>
        /// Wandelt die Kamera in einen XML-Knoten um.
        /// </summary>
        /// <param name="xmlDocument">Das zugrunde liegende XML-Dokument.</param>
        /// <returns>Der XML-Knoten.</returns>
        internal XmlNode ToXml(XmlDocument xmlDocument)
        {
            XmlNode node = xmlDocument.CreateNode(XmlNodeType.Element, "Camera", null);
            XmlNode nodeFocusOfView = xmlDocument.CreateNode(XmlNodeType.Element, "FocusOfView", null);
            nodeFocusOfView.InnerText = fovAngle.ToString();
            node.AppendChild(position.ToXml(xmlDocument, "Position"));
            node.AppendChild(lookAtPosition.ToXml(xmlDocument, "LookAtPosition"));
            node.AppendChild(topDirection.ToXml(xmlDocument, "TopDirection"));
            node.AppendChild(nodeFocusOfView);
            return node;
        }
        #endregion Typenumwandlungen
        #endregion Methoden
    }
}
